#!/usr/bin/env python
#
# Copyright 2014 Ville Rantanen
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import sys,os
import re
import urllib
import shutil
import csv
import subprocess
import string
from math import ceil
from datetime import datetime

# (c) ville.rantanen@helsinki.fi

__version__='2.20140307'

FILECONFIG=".config"
FILEDESC="descriptions.csv"
FILEDESCJSON="descriptions.json"
FILEINFO="info.txt"
SAVEDCONFIG="""attachments=boolean
gallery=string
infofile=string
parent=string
reverse=boolean
timesort=boolean
clean=boolean
force=boolean
gravity=string
link=boolean
thumbs=boolean
width=string""".split('\n')
CONFIGCOMMENTS="""
config values:
gallery: Name of the gallery
infofile: Name of the infofile, inserted in beginning of the main page
parent: String URL pointing to parent folder
reverse: Sort reverse
timesort: Sort by timestamp
clean: Delete unused thumbnails
force: Force recreate thumbnails
gravity: ImageMagick option for creating thumbnails, e.g. North,East,Center 
link: Medium sized images are symbolic links to original
thumbs: Build medium sized and thumbnail images.
width: Medium images longer axis in pixels 
""".split('\n')
webfilesearch=re.compile('.*index.html$|gallerystyle.css$|galleryscript.js$|'+FILEDESCJSON+'$|'+FILEDESC+'$|^'+FILEINFO+'$|\..*',re.I)
imagesearch=re.compile('.*\.jpg$|.*\.jpeg$|.*\.gif$|.*\.png$|.*\.svg$|.*\.pdf$',re.I)
vectorsearch=re.compile('.*\.svg$|.*\.pdf$',re.I)
nonconvertiblesearch=re.compile('.*\.html$|.*\.htm$|.*\.php$',re.I)
#gifsearch=re.compile('.*gif$',re.I)
excludepaths=re.compile('_med|_tn|\..*')
doublequotes=re.compile('"')
singlequotes=re.compile("'")
stripquotes=re.compile('^"|"$')

def getheader(path,parent,title=""):
    if title=="":
        title=unicode(os.path.basename(path),encoding="utf8").encode('ascii', 'xmlcharrefreplace')
    return '''<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<HTML>
<HEAD>
<TITLE>'''+title+'''</TITLE>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="'''+parent+'''gallerystyle.css">
<script language="javascript" src="'''+parent+'''galleryscript.js"></script>
</HEAD>
<BODY>
'''
def getfooter():
    return '''
<div id="footer">Generated with Qalbum '''+__version__+''' ('''+datetime.today().strftime("%y-%m-%d %H:%M")+''') <a href="http://code.google.com/p/qalbum/wiki/Usage" target="_TOP">Need help?</a></div>
<script language="javascript">setup();</script>
</BODY>
</HTML>
'''

def getimagelist(path,options=False):
    ''' Returns a list of images matching the regex '''
    list=os.listdir(path)
    imgs=[]
    for f in list:
        if (imagesearch.match(f)) and (os.path.isfile(os.path.join(path,f))):
            imgs.append(f)
    if options:
        if options.timesort:
            imgs.sort(key=lambda f: os.path.getmtime(os.path.join(path, f)),reverse=options.reverse)
        else:
            imgs.sort(reverse=options.reverse,key=lambda x: natural_sort_key(x))
    else:
        imgs.sort(key=lambda x: natural_sort_key(x))
    return imgs

def getnonconvertiblelist(path,options=False):
    ''' Returns a list of files matching the nonconvertible regex '''
    list=os.listdir(path)
    files=[]
    for f in list:
        if (nonconvertiblesearch.match(f)) and (os.path.isfile(os.path.join(path,f))) and not (webfilesearch.match(f)):
            files.append(f)
    if options:
        if options.timesort:
            files.sort(key=lambda f: os.path.getmtime(os.path.join(path, f)),reverse=options.reverse)
        else:
            files.sort(reverse=options.reverse,key=lambda x: natural_sort_key(x))
    else:
        files.sort(key=lambda x: natural_sort_key(x))
    return files

def getfiletimes(path,list):
    ''' Returns a list of modification times '''
    times=[]
    for p in list:
        times.append(int(os.path.getmtime(os.path.join(path,p))))
    return times

def getfilesizes(path,list):
    ''' Returns a list of sizes '''
    sizes=[]
    for p in list:
        sizes.append(int(os.path.getsize(os.path.join(path,p))))
    return sizes

def getnonimagelist(path,options):
    ''' Returns a list of files not matching the image match regex '''
    list=os.listdir(path)
    files=[]
    if not options.attachments:
        return files
    for f in list:
        if (not webfilesearch.match(f)) and (not imagesearch.match(f)) and (os.path.isfile(os.path.join(path,f))):
            files.append(f)
    if options.timesort:
        files.sort(key=lambda f: os.path.getmtime(os.path.join(path, f)),reverse=options.reverse)
    else:
        files.sort(reverse=options.reverse,key=lambda x: natural_sort_key(x))
    return files
    
def getpathlist(path,options=False):
    ''' Returns a list of subfolders not matching the exclusion regex '''
    list=os.listdir(path)
    paths=[]
    for d in list:
        if (not excludepaths.match(d)) and (os.path.isdir(os.path.join(path,d))):
            paths.append(d)
    if options:
        if options.timesort:
            paths.sort(key=lambda f: os.path.getmtime(f),reverse=options.reverse)
        else:
            paths.sort(reverse=options.reverse,key=lambda x: natural_sort_key(x))
    else:
        paths.sort(key=lambda x: natural_sort_key(x))
    return paths

def pathscript(path,list):
    ''' Returns the javascript string of pathlist and pathimage arrays '''
    scrstr='<script language="javascript">var pathlist=['
    elements=[]
    for p in list:
        imglist=getimagelist(os.path.join(path,p))
        pathlist=getpathlist(os.path.join(path,p))
        this_str='{ name:"'+unicode(p,encoding="utf8").encode('ascii', 'xmlcharrefreplace')+'", '
        this_str+='size:'+str(len(imglist)+len(pathlist))+', '
        if len(imglist)>0:
            this_str+='image:"'+unicode(p,encoding="utf8").encode('ascii', 'xmlcharrefreplace')+'/_tn/tn_'+unicode(imglist[0],encoding="utf8").encode('ascii', 'xmlcharrefreplace')+'.jpg"}'
        else:
            this_str+='image:"" }'
        elements.append(this_str)
    scrstr+=','.join(elements)+'];</script>'
    return scrstr

def pathlinks(path,list):
    ''' Returns the HTML string of subfolders '''
    if len(list)==0:
        return '<div id="pathcontainer"></div>'
    pathstr='<div id="pathcontainer">'
    pathstr+='<h1>Subfolders</h1>'
    for p in list:
        nice=nicestring(p)
        imglist=getimagelist(os.path.join(path,p))
        nsum=str(len(imglist))
        imgstr=""
        if len(imglist)>0:
            imgstr='<span class="pathbox" style="background-image:url(\''+urllib.quote(p)+'/_tn/tn_'+urllib.quote(imglist[0])+'.jpg\');">'
        else:
            imgstr='<span class="pathbox">'
        pathstr+='<a title="'+unicode(p,encoding="utf8").encode('ascii', 'xmlcharrefreplace')+'" href="'+urllib.quote(p)+'/index.html">'+imgstr+'<span class="pathlink"><span class="pathlinktext">'+unicode(nice,encoding="utf8").encode('ascii', 'xmlcharrefreplace')+' ('+nsum+')</span></span></span></a>';
    pathstr+='</script>'
    pathstr+='</div>'
    return pathstr

def imagescript(path,list):
    ''' Returns the javascript string of imagelist and imagedesc '''
    strout='<script language="javascript">var imagelist=['
    descriptions=getdescriptions(path,list)
    times=getfiletimes(path,list)
    sizes=getfilesizes(path,list)
    n=0
    elements=[]
    for i in list:
        try:
            desc=singlequotes.sub("\\'",unicode(descriptions[n],encoding="utf8").encode('ascii', 'xmlcharrefreplace'))
        except:
            desc=singlequotes.sub("\\'",filter(lambda x: x in string.printable, descriptions[n]).encode('ascii', 'xmlcharrefreplace'))
        this_str='\n{name:"'+unicode(i,encoding="utf8").encode('ascii', 'xmlcharrefreplace')+'", '
        this_str+='desc:\''+desc+'\', '
        this_str+='size:\''+str(sizes[n])+'\', '
        this_str+='time:'+str(times[n])+'}'
        elements.append(this_str)
        n+=1
    strout+=','.join(elements)+'];</script>'
    return strout

def imagelinks(path,list):
    ''' Returns the HTML string of images '''
    if len(list)==0:
        return '<div id="thumbcontainer"></div>'
    strout='<div id="thumbcontainer"><noscript>'
    strout+='<h1>Images</h1>'
    descriptions=getdescriptions(path,list)
    n=0
    for i in list:
        nice=nicestring(i)
        try:
            desc=doublequotes.sub('',unicode(descriptions[n],encoding="utf8").encode('ascii', 'xmlcharrefreplace'))
        except:
            desc=doublequotes.sub('',filter(lambda x: x in string.printable, descriptions[n]).encode('ascii', 'xmlcharrefreplace'))

        strout+='<span class="imagebox thumbbox" id="n'+str(n)+'"><a href="'+urllib.quote(i)+'"><img class="thumbimage" "title="'+desc+'" src="_tn/tn_'+urllib.quote(i)+'.jpg"><br/>'+unicode(nice,encoding="utf8").encode('ascii', 'xmlcharrefreplace')+'</a></span>'
        n+=1
    strout+='</noscript></div>'
    return strout

def filescript(path,list):
    ''' Returns the javascript string of filelist '''
    strout='<script language="javascript">var filelist=['
    elements=[];
    for i in list:
        elements.append('"'+unicode(i,encoding="utf8").encode('ascii', 'xmlcharrefreplace')+'"')
    strout+=','.join(elements)+'];</script>'
    return strout

def filelinks(path,list):
    ''' Returns the HTML string of non image files '''
    strout='<div id="attachmentcontainer">'
    if len(list)>0:
        strout+='<h2>Attachments</h1>'
    n=0
    for i in list:
        size=sizestring(os.path.getsize(os.path.join(path,i)))
        strout+='<span class="attachmentbox" id="a'+str(n)+'"><a href="'+urllib.quote(i)+'">'+unicode(i,encoding="utf8").encode('ascii', 'xmlcharrefreplace')+' ['+size+']</a></span>'
        n+=1
    strout+='</div>'
    return strout

def cleanthumbs(path):
    ''' clears _med and _tn for unused thumbs '''
    print('clearing unused thumbs...')
    if os.path.exists(os.path.join(path,'_tn')):
        clearfolder(path,os.path.join(path,'_tn'),re.compile("(^tn_)(.*)(.jpg)"))
    if os.path.exists(os.path.join(path,'_med')):
        clearfolder(path,os.path.join(path,'_med'),re.compile("(^med_)(.*)(.jpg)"))
    return

def clearfolder(path,tnpath,regex):
    ''' clears given folder '''
    list=getimagelist(tnpath)
    for i in list:
        f=regex.match(i)
        try: 
            if not os.path.exists(os.path.join(path,f.group(2))):
                print('removing '+i)
                os.remove(os.path.join(tnpath,i))
        except:
            continue
    return

def createthumbs(path,list,options):
    ''' Runs imagemagick Convert to create medium sized and thumbnail images '''
    if len(list)==0:
        return
    if not os.path.exists(os.path.join(path,'_tn')):
        os.mkdir(os.path.join(path,'_tn'))
    if not os.path.exists(os.path.join(path,'_med')):
        os.mkdir(os.path.join(path,'_med'))
    n=1
    nsum=len(list)
    r=str(options.width)
    res=r+'x'+r+'>'
    for i in list:
        outmedium=os.path.join(path,'_med','med_'+i+'.jpg')
        outthumb=os.path.join(path,'_tn','tn_'+i+'.jpg')
        inpath=os.path.join(path,i)
        if (options.force) and os.path.exists(outmedium):
            os.unlink(outmedium)
        if (options.force) and os.path.exists(outthumb):
            os.unlink(outthumb)
        if (not os.path.exists(outmedium)):
            print('Medium.. '+i+' '+str(n)+'/'+str(nsum))
            create_medium_bitmap(inpath,outmedium,r,link=options.link,vector=vectorsearch.match(i))
        if (not os.path.exists(outthumb)):
            print('Thumbnail.. '+i+' '+str(n)+'/'+str(nsum))
            create_thumb_bitmap(outmedium,outthumb,vector=vectorsearch.match(i),gravity=options.gravity)
        n+=1
    return

def create_medium_bitmap(infile,outfile,r,link=False,vector=False):
    if link:
        os.symlink('../'+os.path.basename(infile),outfile)
        return
    res=r+'x'+r+'>'
    if vector:
        convargs=['convert','-density','300x300',infile+'[0]','-background','white','-flatten','-resize',res,'-quality','97',outfile]
    else:
        convargs=['convert','-define','jpeg:size='+r+'x'+r,infile+'[0]','-background','white','-flatten','-resize',res,'-quality','85',outfile]
    convp=subprocess.call(convargs)
    return

def create_thumb_bitmap(infile,outfile,vector=False,gravity='Center'):
    if vector:
        convargs=['convert','-density','300x300',infile,'-background','white','-flatten','-thumbnail','90x90^','-gravity',gravity,'-crop','90x90+0+0','+repage','-quality','75',outfile]
    else:
        convargs=['convert','-define','jpeg:size=300x300',infile,'-background','white','-flatten','-thumbnail','90x90^','-gravity',gravity,'-crop','90x90+0+0','+repage','-quality','75',outfile]
    convp=subprocess.call(convargs)
    return

def getdescriptionsjson(path,list):
    ''' Read descriptions.json for descriptions of filenames.
        Missing descriptions are replaced with the file name. '''
    import json
    if not os.path.exists(os.path.join(path,FILEDESCJSON)):
        return list
    desc=[i for i in list]
    jsondesc = json.load(open(os.path.join(path,FILEDESCJSON),'rb'))
    for key, value in jsondesc.items():
        if key in list:
            i=list.index(stripquotes.sub('',key))
            desc[i]=stripquotes.sub('',value)
    return desc

def getdescriptions(path,list):
    ''' Read descriptions.csv file and returns a list of descriptions.
        If descriptions.csv does not exist, read descriptions.json
        instead.
        Missing descriptions are replaced with the file name. '''
    if not os.path.exists(os.path.join(path,FILEDESC)):
        return getdescriptionsjson(path,list)
    desc=[i for i in list]
    reader = csv.reader(open(os.path.join(path,FILEDESC),'rb'),
                        delimiter='\t',
                        doublequote=False,
                        escapechar='\\',
                        quoting=csv.QUOTE_NONE)
    for row in reader:
        if len(row)>1:
            if row[0] in list:
                i=list.index(stripquotes.sub('',row[0]))
                desc[i]=stripquotes.sub('',row[1])
    return desc

def getinfo(path,options):
    ''' Read info.txt file and returns the content.
        Missing info file returns empty string. '''
    if not os.path.exists(os.path.join(path,options.infofile)):
        return ''
    reader = open(os.path.join(path,options.infofile),'r')    
    return unicode(reader.read(),encoding="utf8",errors="ignore").encode('ascii','xmlcharrefreplace')

def crumblinks(crumbs,title,parent):
    ''' Create the HTML string for crumb trails '''

    strout='<div id="crumbcontainer">'
    if parent:
        if not parent.startswith('http://'):
            parent="../"*(len(crumbs))+parent
        strout+='<a href="'+parent+'">'+'Home'.encode('ascii', 'xmlcharrefreplace')+'</a>: '
    i=1
    for c in crumbs:
        cname=os.path.basename(c)
        if i==1:
            cname=title
        cdepth=len(crumbs)-i
        clink="../"*cdepth
        strout+='<a href="'+clink+'index.html">'+unicode(cname,encoding="utf8").encode('ascii', 'xmlcharrefreplace')+'</a>: '
        i+=1
    strout+='</div>'
    return strout

def nicestring(s):
    ''' Returns a nice version of a long string '''
    if len(s)<20:
        return s
    s=s.replace("_"," ")
    s=s.replace("-"," ")
    if len(s)>30:
        s=s[0:26]+".."+s[-3:]
    return s

def sizestring(size):
    ''' Returns human readable file size string '''
    for x in ['b','kb','Mb','Gb','Tb']:
        if size < 1024.0:
            if (x=='b') | (x=='kb'):
                return "%d%s" % (size, x)
            else:
                return "%3.1f%s" % (size, x)
        size /= 1024.0

def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
    ''' Natural sort / Claudiu@Stackoverflow '''
    return [int(text) if text.isdigit() else text.lower()
            for text in re.split(_nsre, s)]    

def traverse(path,crumbs,inputs,options):
    ''' The recursive main function to create the index.html and seek sub folders '''
    
    print(path)
    if (not options.recurselink) and (os.path.islink(path)):
        print('Not recursing, is a link')
        return
    if len(crumbs)==1:
        header=getheader(path,'../'*(len(crumbs)-1),inputs[0][1])
    else:
        header=getheader(path,'../'*(len(crumbs)-1))
    if not os.path.exists(os.path.join(path,'../'*(len(crumbs)-1),'galleryscript.js')):
        print('Warning, no (relative path) galleryscript! '+os.path.join(path,'../'*(len(crumbs)-1),'galleryscript.js'))
        #depth=0
        #while not os.path.exists(os.path.join(path,'../'*(depth),'galleryscript.js')):
        #    print(os.path.join(path,'../'*(depth)))
        #    depth+=1
        #header=getheader(path,'../'*(depth))
    
    #print('Depth: '+str(len(crumbs)))
    pathlist=getpathlist(path,options)
    imagelist=getimagelist(path,options)
    if options.clean:
        cleanthumbs(path)
    if options.thumbs:
        createthumbs(path,imagelist,options)
    imagelist.extend(getnonconvertiblelist(path,options))
    filelist=getnonimagelist(path,options)
    print(str(len(pathlist))+' paths, '+str(len(imagelist))+' images, '+str(len(filelist))+' other files')
    crumbstring=crumblinks(crumbs,options.gallery,options.parent)
    pathjs=pathscript(path,pathlist)
    pathstring=pathlinks(path,pathlist)
    filestring=filelinks(path,filelist)
    #filejs=filescript(path,filelist) # Filelist is not currently used in javascript
    imagestring=imagelinks(path,imagelist)
    imagejs=imagescript(path,imagelist)

    f=open(os.path.join(path,"index.html"),"w")
    f.write(header)
    f.write('<div id="preloadcontainer"></div>')
    f.write(pathjs)
    f.write(imagejs)
    #f.write(filejs)
    f.write(crumbstring)
    f.write(pathstring)
    f.write('<div id="imagecontainer">'+getinfo(path,options)+'</div>')
    f.write('<div id="desccontainer"></div>')
    f.write(imagestring)
    f.write(filestring)
    f.write('<div id="listcontainer"></div>')
    f.write(getfooter())
    f.close()
    
    for p in pathlist:
        nextcrumbs=[i for i in crumbs]
        nextcrumbs.append(os.path.join(path,p))
        traverse(os.path.join(path,p),nextcrumbs,inputs,options)
    return

def setupoptions():
    ''' Setup the command line options '''
    from argparse import ArgumentParser 
    parser=ArgumentParser()
    parser.add_argument("-v",action='version', version=__version__)
    parser.add_argument("--version",action='version', version=__version__)
    parser.add_argument("-c",action="store_true",dest="writeconfig",default=False,
                      help="Write current configuration to file "+FILECONFIG+
                           ". If file exists, Qalbum and thumbnail handling read from the file, "+
                           "overriding switches.")
    parser.add_argument("-r",action="store_true",dest="reverse",default=False,
                      help="Reverse sort orded")
    parser.add_argument("-L",action="store_false",dest="recurselink",default=True,
                      help="List, but do not recurse in to symbolic link folders")
    parser.add_argument("-s",type=str,dest="style",
                      help="User defined CSS style file.")
    parser.add_argument("-t",action="store_true",dest="timesort",default=False,
                      help="Sort by file modification time")
    parser.add_argument("-a",action="store_false",dest="attachments",default=True,
                      help="Disable attachments")
    parser.add_argument("-i",type=str,dest="infofile",default=FILEINFO,
                      help="File name for info files in all folders. (Default: %(default)s)")
    parser.add_argument("-g",type=str,dest="gallery",default="Gallery",
                      help="Name for the root gallery (Default: %(default)s)")
    parser.add_argument("--gravity",type=str,dest="gravity",default="Center",
                      help="ImageMagick gravity for cropping. (Default: %(default)s)")
    parser.add_argument("-w",type=int,dest="width",default=850,
                      help="Medium image size (Default: %(default)s)")
    parser.add_argument("--no-thumbs",action="store_false",dest="thumbs",default=True,
                      help="Disable thumbnail and medium generation. Build the indexes only.")
    parser.add_argument("-p",type=str,dest="parent",
                      help="Add a ../[PARENT] link to point out from the gallery. If the string starts with http:// it is considered as a static URL, otherwise the relative parent path is assumed.")
    parser.add_argument("startpath",type=str,action="store",default=os.path.abspath('.'),nargs='?',
                      help="Root path of the gallery")
    options=parser.parse_args()
    options.startpath=os.path.abspath(options.startpath)
    options=setupdefaultoptions(options)
    return options                      
    
def setupdefaultoptions(options):
    ''' Adds the missing options for the options object '''
    if 'attachments' not in options:
        options.attachments=True
    if 'clean' not in options:
        options.clean=False
    if 'force' not in options:
        options.force=False
    if 'infofile' not in options:
        options.infofile=FILEINFO
    if 'gallery' not in options:
        options.gallery="Gallery"
    if 'gravity' not in options:
        options.gravity="Center"
    if 'link' not in options:
        options.link=False
    if 'recursive' not in options:
        options.recursive=True
    if 'recurselink' not in options:
        options.recurselink=True
    if 'reverse' not in options:
        options.reverse=False
    if 'style' not in options or options.style is None:
        options.style=os.path.join(os.path.abspath(os.path.dirname(os.path.realpath(sys.argv[0]))),'gallerystyle.css')
    if 'timesort' not in options:
        options.timesort=False
    if 'thumbs' not in options:
        options.thumbs=True
    if 'width' not in options:
        options.width=850
    return options

def readconfig(options):
    """ Set up the options via config file """
    if os.path.exists(FILECONFIG):
        import configobj
        try:
            cfg=configobj.ConfigObj(FILECONFIG, configspec=SAVEDCONFIG, unrepr=True)
        except configobj.UnreprError as err:
            print("Config file "+FILECONFIG+" syntax error!")
            print(err)
            sys.exit(1)
        for opt in cfg.keys():
            setattr(options,opt,cfg[opt])
        print("Read config from file")
                    
    return options

def writeconfig(options):
    """ Write the options to config file """
    import configobj
    cfg=configobj.ConfigObj(configspec=SAVEDCONFIG, unrepr=True)
    cfg.initial_comment=CONFIGCOMMENTS
    cfg.filename=FILECONFIG
    for opt in SAVEDCONFIG:
        optname=opt.split("=")[0]
        cfg[optname]=getattr(options,optname)
    cfg.write()
    print('Wrote '+FILECONFIG)

def execute_plain():
    ''' Main execution function '''
    options=setupoptions()
    options=readconfig(options)
    options=setupdefaultoptions(options)
    if options.writeconfig:
        writeconfig(options)
    # Copy all resources to target folder
    pathname=os.path.dirname(os.path.realpath(sys.argv[0]))
    fullpath=os.path.abspath(pathname)
    if not os.path.exists(options.style):
        raise IOError('File not found: "'+options.style+'"')
    shutil.copyfile(options.style,os.path.join(options.startpath,'gallerystyle.css'))
    shutil.copyfile(os.path.join(fullpath,'galleryscript.js'),os.path.join(options.startpath,'galleryscript.js'))
    
    inputs=[]
    inputs.append((None,options.gallery,None))
    
    traverse(options.startpath,[options.startpath],inputs,options)
    return


if __name__ == "__main__":
    execute_plain()
